Setting up alternatives to Discord

Like i said in the previous article, there are a lot of Discord alternatives. In this article let's talk about the 2 main ones:

Let's get out the elephant in the room... I don't think Matrix will be that used. It's not easy to setup, right now, you have to have a lot of skills aroud Linux, Turn/Stun servers, modifing configs and reading documentation, pocking holes in your firewall... and I can go, unfortunately, on and on.

So let's talk about the easiest of the 2 to self-host

TeamSpeak 6

Still in Beta but stable enought, in my opinion. There are no ways of buying a license still, yeah you will have to pay for using TeamSpeak 6, but there is the "Beta License" available.

If you ever set up the TS3 server it's the same drill, but modernized with docker or the standalone executable.

Before you start

Make sure you have docker installed on your server and a firewall you can open.

Configuration

Create a folder for the TS6 container and create a file named docker-compose.yaml and paste this:

services:
  teamspeak:
    image: teamspeaksystems/teamspeak6-server:latest
    container_name: teamspeak-server
    restart: unless-stopped
    ports:
      - "9987:9987/udp"   # Voice Port
      - "30033:30033/tcp" # File Transfer
      # - "10080:10080/tcp" # Web Query
    environment:
      - TSSERVER_LICENSE_ACCEPTED=accept
    volumes:
      - teamspeak-data:/var/tsserver

volumes:
  teamspeak-data:
    name: teamspeak-data

Now:

Connection

Now you can, with the TS6 client, connect to the IP of the server or, if you want an easier and professional connection:

Matrix

If you want to dig deeper on alternatives there is the Matrix protocol with Synapse (the one we are looking at) or Conduit.

Before you start

Make sure you have a domain set up, podman installed on your server and configured for quadlets, a nginx reverse proxy (make sure that you have the stream plugin installed and enabed), a firewall you can open and... patience.

Configuration

The configuration will be using podman quadlets, because I done my setup with it and it works grate.

TRUN/STUN server

First of all we have to setup a TURN/STUN server, I chose eturnal as it was easier to set up than coturn

So create a folder for eturnal under ./config/containers/systemd, if you are using rootless containers, or /etc/containers/systemd, if you want to run all under root, modify and paste the eturnal.container file:

[Unit]
Description=Turn container

[Container]
Pod=eturnal.pod
ContainerName=eturnal
Image=ghcr.io/processone/eturnal:latest
AutoUpdate=registry
Network=host
DNS=8.8.8.8

Volume=/path/to/eturnal.yml:/etc/eturnal.yml:ro
Secret=ETURNAL_SECRET,type=env,target=ETURNAL_SECRET
Environment=ETURNAL_RELAY_IPV4_ADDR=${PUBLIC_IP} #optional if you have static ip

[Service]
ExecStartPre=/bin/sh -c 'echo "PUBLIC_IP=$(curl -s https://api.ipify.org)" > /tmp/eturnal.env' #optional if you have static ip
EnvironmentFile=/tmp/eturnal.env #optional if you have static ip
Restart=on-failure
TimeoutStartSec=300
 
[Install]
WantedBy=default.target

Set the eturnal.pod file in the same directory:

[Pod]
PodName=eturnal

Create the configuration file of eturnal, eturnal.yml:

# eturnal STUN/TURN server configuration file.
#
# This file is written in YAML. The YAML format is indentation-sensitive, please
# MAKE SURE YOU INDENT CORRECTLY.
#
# See: https://eturnal.net/doc/#Global_Configuration

eturnal:

  ## Shared secret for deriving temporary TURN credentials (default: $RANDOM):
  #secret: "long-and-cryptic"

  listen:
    -
      ip: "0.0.0.0"
      port: 3478
      transport: udp
    -
      ip: "0.0.0.0"
      port: 3479
      transport: tcp
   # -
   #   ip: "::"
   #   port: 5349
   #   transport: tls

  ## TLS certificate/key files (must be readable by 'eturnal' user!):
  ## tls_crt_file: /etc/eturnal/tls/crt.pem
  ## tls_key_file: /etc/eturnal/tls/key.pem

  ## The server's public IPv4 address (default: autodetected):
  #relay_ipv4_addr: ""
  ## The server's public IPv6 address (optional):
  #relay_ipv6_addr: "2001:db8::4"

  ## UDP relay port range (usually, several ports per A/V call are required):
  relay_min_port: 49152     # This is the default.
  relay_max_port: 65535     # This is the default.

  ## Reject TURN relaying to the following addresses/networks:
  blacklist_peers:
    - recommended           # Expands to various addresses/networks recommended
    - "127.0.0.0/8"
    - "10.0.0.0/8"
    - "172.16.0.0/12"
    - "192.168.0.0/16"
                            # to be blocked. This is the default.
  ## If 'true', close established calls on expiry of temporary TURN credentials:
  strict_expiry: false      # This is the default.

  ## Logging configuration:
  log_level: info           # critical | error | warning | notice | info | debug
  log_rotate_size: 10485760 # 10 MiB (default: unlimited, i.e., no rotation).
  log_rotate_count: 10      # Keep 10 rotated log files.
  #log_dir: stdout          # Enable for logging to the terminal/journal.

  ## See: https://eturnal.net/doc/#Module_Configuration
  modules:
    mod_log_stun: {}        # Log STUN queries (in addition to TURN sessions).
    #mod_stats_prometheus:  # Expose STUN/TURN and VM metrics to Prometheus.
    #  ip: any              # This is the default: Listen on all interfaces.
    #  port: 8081           # This is the default.
    #  tls: false           # This is the default.
    #  vm_metrics: true     # This is the default.

Create the secret with

printf "CHANGE_ME1!" | podman secret create ETURNAL_SECRET -

Configure the reverse proxy, for me nginx, creating in /etc/nginx/conf.d/ the turn.conf file:

server {
   listen 443 ssl;
   server_name turn.domain.com;
   ssl_certificate /path/to/cert.domain.com.pem;
   ssl_certificate_key /path/to/certkey/turn.domain.com.key;
   add_header X-Forwarded-Port $server_port;
   add_header X-Forwarded-Proto https;
   location / {
       proxy_pass http://machineip:3479;
       proxy_set_header Host $host;
       proxy_set_header X-Real-IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Forwarded-Proto https;
    }
  }

and open the 3478/udp port on the router firewall.

Synapse configuration

So create a folder for synapse under ./config/containers/systemd, if you are using rootless containers, or /etc/containers/systemd, if you want to run all under root, modify and paste the synapse.container file:

[Unit]
Description=Synapse container
Requires=synapse-postgres.service
After=eturnal.service synapse-postgres.service

[Container]
Pod=synapse.pod
ContainerName=synapse
Image=ghcr.io/element-hq/synapse:latest
AutoUpdate=registry
Exec=generate #this will generate the config for synapse in /data/homeserver.yaml. COMMENT THIS AFTER THE FIRST LAUNCH!
HealthCmd=curl -f http://127.0.0.1:8008/_matrix/client/versions
 
Volume=/path/to/synapse/files:/data

Environment=SYNAPSE_REPORT_STATS=no
Environment=SYNAPSE_SERVER_NAME=matrix.domain.com #modify with your matrix domain
Environment=SYNAPSE_NO_TLS=""
Environment=SYNAPSE_HTTP_PORT=8008
Secret=SYNAPSE_POSTGRES_PASSWORD,type=env,target=POSTGRES_PASSWORD
Environment=POSTGRES_USER=synapse
Environment=POSTGRES_DB=synapse
Environment=POSTGRES_HOST=synapse-postgres
Environment=SYNAPSE_MAX_UPLOAD_SIZE=1G
Environment=SYNAPSE_TURN_URIS='["turn:turn.domain.com:3478?transport=udp", "turn:turn.domain.com:443?transport=tcp"]' #modify with your turn domain
Secret=SYNAPSE_TURN_SECRET,type=env,target=SYNAPSE_TURN_SECRET
Secret=SYNAPSE_REGISTRATION_SHARED_SECRET,type=env,target=SYNAPSE_REGISTRATION_SHARED_SECRET

[Service]
Restart=on-failure
TimeoutStartSec=300
 
[Install]
WantedBy=default.target

In the same folder create the synapse-postres.container file, paste and modify:

[Unit]
Description=Postgresql db for synapse
 
[Container]
Pod=synapse.pod
ContainerName=synapse-postgres
Image=docker.io/library/postgres:17-alpine
AutoUpdate=registry

HealthCmd=pg_isready -U synapse -d synapse
 
Volume=/path/to/synapse/db:/var/lib/postgresql/data

Environment=POSTGRES_USER=synapse
Secret=SYNAPSE_POSTGRES_PASSWORD,type=env,target=POSTGRES_PASSWORD
Environment=POSTGRES_DB=synapse
Environment=POSTGRES_INITDB_ARGS="--encoding=UTF-8 --lc-collate=C --lc-ctype=C"

[Service]
Restart=on-failure
TimeoutStartSec=300
 
[Install]
WantedBy=default.target

Configure the .network file:

[Unit]
Description=Synapse Network
After=podman-user-wait-network-online.service

[Network]
NetworkName=synapse
Subnet=10.90.2.0/30
Gateway=10.90.2.2
DNS=9.9.9.11

[Install]
WantedBy=default.target

and the .pod file

[Pod]
Network=synapse.network
PodName=synapse
PublishPort=8008:8008
PublishPort=8070:8080

Create the necessary secrets with:

printf "CHANGE_ME1!" | podman secret create SYNAPSE_POSTGRES_PASSWORD -
printf "CHANGE_ME1!" | podman secret create SYNAPSE_TURN_SECRET -
printf "CHANGE_ME1!" | podman secret SYNAPSE_REGISTRATION_SHARED_SECRET ETURNAL_SECRET -

Configure nginx creating in /etc/nginx/conf.d/ the matrix.conf file:

server {
  listen 443 ssl http2;
  listen 8448 ssl http2 default_server;
  server_name matrix.domain.com;

  # Nginx standard body size is 1MB, which is quite small for media uploads
  # Increase this to match the max_request_size in your tuwunel.toml
  client_max_body_size 100M;

  location ~ ^(/_matrix|/_synapse/client) {
        proxy_pass http://127.0.0.1:8008;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Nginx default is 1M; Matrix needs more for media uploads
        client_max_body_size 50M;

        # Prevents Nginx from buffering the response
        proxy_buffering off;
    }

  # TLS configuration (Let's Encrypt example using certbot)
  ssl_certificate /path/to/cert/matrix.domain.com.pem;
  ssl_certificate_key /path/to/certkey/matrix.domain.com.key;
 
 location /.well-known/matrix/client {
    add_header Content-Type application/json;
    add_header Access-Control-Allow-Origin *;
    return 200 '{"m.homeserver": {"base_url": "https://matrix.domain.com"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "https://matrixrtc.domain.com"}]}';
}

location /.well-known/matrix/server {
    return 200 '{"m.server": "matrix.domain.com:443"}';
    add_header Content-Type application/json;
}
}

Now you can start the synapse-pod with:

systemctl --user daemon-reload
systemctl --user start synapse-pod

Comment line 11 in the synapse.container file. Reload systemd unit files modify the homeserver.yaml in the data folder of synapse:

database:
  name: psycopg2
  args:
    user: synapse
    password: "CHANGE_ME1!" #modify with the postgres password that we created before
    database: synapse
    host: synapse-postgres
    port: 5432
    cp_min: 5
    cp_max: 10

Add at the end of the file:

event_cache_size: "10K"
rc_messages_per_second: 0.2
rc_message_burst_count: 10.0
federation_rc_window_size: 1000
federation_rc_sleep_limit: 10
federation_rc_sleep_delay: 500
federation_rc_reject_limit: 50
federation_rc_concurrent: 3
max_upload_size: "1G"
max_image_pixels: "32M"
dynamic_thumbnails: false
thumbnail_sizes:
  - width: 32
    height: 32
    method: crop
  - width: 96
    height: 96
    method: crop
  - width: 320
    height: 240
    method: scale
  - width: 640
    height: 480
    method: scale
  - width: 800
    height: 600
    method: scale
url_preview_enabled: False
max_spider_size: "10M"
turn_uris:
  - "turn:turn.domain.com:3478?transport=udp"
  - "turn:turn.domain.com:443?transport=tcp"
enable_registration: true
experimental_features:
  # MSC3266: Room summary API. Used for knocking over federation
  msc3266_enabled: true
  # MSC4222 needed for syncv2 state_after. This allow clients to
  # correctly track the state of the room.
  msc4222_enabled: true

# The maximum allowed duration by which sent events can be delayed, as
# per MSC4140.
max_event_delay_duration: 24h

rc_message:
  # This needs to match at least e2ee key sharing frequency plus a bit of headroom
  # Note key sharing events are bursty
  per_second: 0.5
  burst_count: 30

rc_delayed_event_mgmt:
  # This needs to match at least the heart-beat frequency plus a bit of headroom
  # Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
  per_second: 1
  burst_count: 20
trusted_proxies:
  - 10.0.0.0/8    # Or your specific Podman network range
  - 127.0.0.1
enable_room_list_search: true
room_list_publication_rules:
  - user_id: '@admin:matrix.domain.com' #edit with the admin account
    action: allow
recaptcha_public_key: "" #set the recaptcha public key from google's recaptcha, cloudflare, etc...
recaptcha_private_key: "" #set the recaptcha private key from google's recaptcha, cloudflare, etc...
enable_registration_captcha: true

Than create an admin user with:

podman exec -it synapse register_new_matrix_user -c /data/homeserver.yaml http://localhost:6167

Grab a matrix client and login.

If you chose Element as a client you will have the option to create a video room, a persistent room that you can join with audio and video, but if you try to join you will receive an error that is because we will need to setup a container with LiveKit

LiveKit configuration

Create in the same directory of the synapse.container multiple .container files:

[Container]
Pod=synapse.pod
ContainerName=synapse-rtc-jwt
Image=ghcr.io/element-hq/lk-jwt-service:latest
AutoUpdate=registry
 
Environment=LIVEKIT_JWT_BIND=0.0.0.0:8080
Environment=LIVEKIT_URL=wss://livekit.domain.com
Environment=LIVEKIT_KEY=devkey
Environment=LIVEKIT_FULL_ACCESS_HOMESERVERS=https://matrix.domain.com
Secret=LIVEKIT_SECRET,type=env,target=LIVEKIT_SECRET

[Service]
Restart=on-failure
TimeoutStartSec=300
 
[Install]
WantedBy=default.target
[Unit]
Description=Synapse rtc call container
After=synapse-rtc-jwt.service
[Container]
Pod=synapse.pod
ContainerName=synapse-rtc-call
Image=docker.io/livekit/livekit-server:latest
AutoUpdate=registry
Exec=--config /etc/livekit.yaml
Network=host
Volume=/path/to/livekit.yaml:/etc/livekit.yaml

[Service]
Restart=on-failure
TimeoutStartSec=300
 
[Install]
WantedBy=default.target

Create a livekit.yaml file in the same directory:

port: 7880
bind_addresses:
  - "0.0.0.0"
rtc:
  tcp_port: 7881
  port_range_start: 50100
  port_range_end: 50200
  use_external_ip: true 
room:
  auto_create: true
logging:
  level: info
turn:
  enabled: false
  domain: turn.domain.com
  tls_port: 443
  udp_port: 3478
  external_tls: true
keys:
  devkit : "CHANGE_ME1!"

Configure nginx creating in /etc/nginx/conf.d/ the matrix-rtc.conf file:

server {
listen 443 ssl http2;
server_name matrixrtc.domain.com;

location /sfu/get {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    
    proxy_pass http://127.0.0.1:8070/sfu/get;
}
  # TLS configuration (Let's Encrypt example using certbot)
  ssl_certificate /path/to/cert/matrixrtc.domain.com.pem;
  ssl_certificate_key /path/to/certkey/matrixrtc.domain.com.key;
}

server {
  listen 443 ssl http2;
  server_name livekit.domain.com;

  location / {
    proxy_pass http://127.0.0.1:7880; # LiveKit's internal port
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
    proxy_buffering off; # Prevents Nginx from "holding" the video packets
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}
  # TLS configuration (Let's Encrypt example using certbot)
  ssl_certificate /path/to/cert/livekit.domain.com.pem;
  ssl_certificate_key /path/to/certkey/livekit.domain.com.key;
}

After all of this we need a certificate for:

You can request it/them from let's encrypt with certbot

I request them one by one with this script:

#!/bin/bash

#modify this
DOMAIN_1="matrix.domain.com"
DOMAIN_2="turn.domain.com"
DOMAIN_3="matrixrtc.domain.com"
DOMAIN_4="livekit.domain.com"
EMAIL="your@email.com" 
PATHCERT="path/to/certs"

certbot certonly --standalone --preferred-challenges http -d $DOMAIN_1 --non-interactive --agree-tos --email $EMAIL
certbot certonly --standalone --preferred-challenges http -d $DOMAIN_2 --non-interactive --agree-tos --email $EMAIL
certbot certonly --standalone --preferred-challenges http -d $DOMAIN_3 --non-interactive --agree-tos --email $EMAIL
certbot certonly --standalone --preferred-challenges http -d $DOMAIN_4 --non-interactive --agree-tos --email $EMAIL

cat /etc/letsencrypt/live/$DOMAIN_1/fullchain.pem > $PATHCERT$DOMAIN_1.pem
cat /etc/letsencrypt/live/$DOMAIN_1/privkey.pem > $PATHCERT$DOMAIN_1.key

cat /etc/letsencrypt/live/$DOMAIN_2/fullchain.pem >$PATHCERT$DOMAIN_2.pem
cat /etc/letsencrypt/live/$DOMAIN_2/privkey.pem > $PATHCERT$DOMAIN_2.key

cat /etc/letsencrypt/live/$DOMAIN_3/fullchain.pem > $PATHCERT$DOMAIN_3.pem
cat /etc/letsencrypt/live/$DOMAIN_3/privkey.pem > $PATHCERT$DOMAIN_3.key

cat /etc/letsencrypt/live/$DOMAIN_4/fullchain.pem > $PATHCERT$DOMAIN_4.pem
cat /etc/letsencrypt/live/$DOMAIN_4/privkey.pem > $PATHCERT$DOMAIN_4.key

Modify the synapse.container line 3 to launch the synapse server after the synapse-rtc-jwt.service and synapse-rtc-call.service

...
After=eturnal.service synapse-postgres.service synapse-rtc-jwt.service synapse-rtc-call.service
...

At the end we can launch the pod with:

systemctl --user daemon-reload
systemctl --user start synapse-pod

Login with our client, invite our friends and enjoy our matrix server!

Conclusions

If you choose the Matrix server you will have a complete alternative to Discord ready with the PC app and a mobile app, and of course with FOSS software.

If you'll choose the simpler approach of TeamSpeak 6, I will not blame you, but, at the time of writing, we have no clue to how much a license will be and if and when the mobile app will be available.

I kwow that other softwares are available and self-host but these 2 are the most mature and popular.

I hope to have helped you to set up all of this.

Nastro_